#region Released to Public Domain by Gael Fraiteur
/*----------------------------------------------------------------------------*
 *   This file is part of samples of PostSharp.                                *
 *                                                                             *
 *   This sample is free software: you have an unlimited right to              *
 *   redistribute it and/or modify it.                                         *
 *                                                                             *
 *   This sample is distributed in the hope that it will be useful,            *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of            *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.                      *
 *                                                                             *
 *----------------------------------------------------------------------------*/
#endregion

using System.Reflection;
using PostSharp.CodeModel;
using PostSharp.CodeModel.Helpers;
using PostSharp.Collections;
using PostSharp.Laos.Weaver;
using PostSharp.Samples.Librarian.Framework;

namespace PostSharp.Samples.Librarian.Weaver
{
    /// <summary>
    /// Generate code (specifically the <see cref="Aspectable.AutoGeneratedValidate"/> method)
    /// for the 'Validate' aspect.
    /// </summary>
    internal class ImplementValidableAspectWeaver : TypeLevelAspectWeaver
    {
        public override void Implement()
        {
            ModuleDeclaration module = this.Task.Project.Module;

            TypeDefDeclaration typeDef = (TypeDefDeclaration) this.TargetType;

            // Declare the method.
            MethodDefDeclaration methodDef = new MethodDefDeclaration();
            methodDef.Name = "AutoGeneratedValidate";
            methodDef.Attributes = MethodAttributes.Family | MethodAttributes.ReuseSlot | MethodAttributes.Virtual;
            methodDef.CallingConvention = CallingConvention.HasThis;
            typeDef.Methods.Add( methodDef );
            methodDef.CustomAttributes.Add( this.Task.WeavingHelper.GetDebuggerNonUserCodeAttribute() );

            // Define parameter.
            methodDef.ReturnParameter = new ParameterDeclaration();
            methodDef.ReturnParameter.ParameterType = module.Cache.GetIntrinsic( IntrinsicType.Void );
            methodDef.ReturnParameter.Attributes = ParameterAttributes.Retval;

            // Define the body
            MethodBodyDeclaration methodBody = new MethodBodyDeclaration();
            methodDef.MethodBody = methodBody;
            InstructionBlock instructionBlock = methodBody.CreateInstructionBlock();
            methodBody.RootInstructionBlock = instructionBlock;
            InstructionSequence sequence = methodBody.CreateInstructionSequence();
            instructionBlock.AddInstructionSequence( sequence, NodePosition.After, null );
            InstructionWriter writer = this.Task.InstructionWriter;
            writer.AttachInstructionSequence( sequence );

            // Find the base method.
            IMethod baseValidateMethod = null;
            IType baseTypeCursor = typeDef.BaseType;
            MethodSignature methodSignature =
                new MethodSignature( CallingConvention.HasThis, module.Cache.GetIntrinsic( IntrinsicType.Void ),
                                     new ITypeSignature[0], 0 );

            while ( baseValidateMethod == null )
            {
                TypeDefDeclaration baseTypeCursorTypeDef = baseTypeCursor.GetTypeDefinition();

                baseValidateMethod =
                    baseTypeCursorTypeDef.Methods.GetMethod( "AutoGeneratedValidate",
                                                             methodSignature.Translate( baseTypeCursorTypeDef.Module ),
                                                             BindingOptions.OnlyExisting |
                                                             BindingOptions.DontThrowException );

                baseTypeCursor = baseTypeCursorTypeDef.BaseType;
            }

            // TODO: support generic base types.

            // Call the base method.
            writer.EmitInstruction( OpCodeNumber.Ldarg_0 );
            writer.EmitInstructionMethod( OpCodeNumber.Call, (IMethod) baseValidateMethod.Translate( typeDef.Module ) );

            // Make an array with the boxed field values.
            TypeValidationAspect aspect = (TypeValidationAspect) this.Aspect;
            LocalVariableSymbol fieldValuesArrayLocal = instructionBlock.DefineLocalVariable(
                module.Cache.GetType( typeof(object[]) ), "fieldValues" );
            writer.EmitInstructionInt32( OpCodeNumber.Ldc_I4, aspect.Validators.Count );
            writer.EmitInstructionType( OpCodeNumber.Newarr, module.Cache.GetIntrinsic( IntrinsicType.Object ) );
            writer.EmitInstructionLocalVariable( OpCodeNumber.Stloc, fieldValuesArrayLocal );

            int i = 0;
            foreach ( FieldValidationAttribute validator in aspect.Validators )
            {
                FieldDefDeclaration fieldDef = typeDef.Fields.GetByName( validator.TargetField.Name );
                IField fieldSpec = GenericHelper.GetFieldCanonicalGenericInstance( fieldDef );

                writer.EmitInstructionLocalVariable( OpCodeNumber.Ldloc, fieldValuesArrayLocal );
                writer.EmitInstructionInt32( OpCodeNumber.Ldc_I4, i );
                writer.EmitInstruction( OpCodeNumber.Ldarg_0 );
                writer.EmitInstructionField( OpCodeNumber.Ldfld, fieldSpec );
                this.Task.WeavingHelper.ToObject( fieldSpec.FieldType, writer );
                writer.EmitInstruction( OpCodeNumber.Stelem_Ref );

                i++;
            }

            // Get the validator method.
            IMethod validateMethod = (IMethod) module.Cache.GetItem(
                                                   delegate( ModuleDeclaration theModule )
                                                       {
                                                           return
                                                               theModule.FindMethod(
                                                                   typeof(TypeValidationAspect).GetMethod( "Validate" ),
                                                                   BindingOptions.Default );
                                                       } );

            // Call the validator.
            writer.EmitInstructionField( OpCodeNumber.Ldsfld, this.AspectRuntimeInstanceField );
            writer.EmitInstructionLocalVariable( OpCodeNumber.Ldloc, fieldValuesArrayLocal );
            writer.EmitInstructionMethod( OpCodeNumber.Callvirt, validateMethod );

            writer.EmitInstruction( OpCodeNumber.Ret );
            writer.DetachInstructionSequence();
        }
    }
}